package trikita.anvilgen

import com.squareup.javapoet.*
import com.sun.org.apache.xpath.internal.operations.Bool
import groovy.lang.Closure
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.lang.Deprecated
import java.lang.reflect.Method
import java.net.URL
import java.net.URLClassLoader
import java.util.*
import java.util.jar.JarFile
import javax.lang.model.element.Modifier

open class DSLGeneratorTask : DefaultTask() {

    lateinit var jarFile: File
    lateinit var dependencies: List<File>
    lateinit var taskName: String
    lateinit var javadocContains: String
    lateinit var outputDirectory: String
    lateinit var outputClassName: String
    lateinit var packageName: String
    var superclass: ClassName? = null

    @TaskAction
    fun generate() {
        var attrsBuilder = TypeSpec.classBuilder(outputClassName)
                .addJavadoc("DSL for creating views and settings their attributes.\n" +
                        "This file has been generated by " +
                        "{@code gradle $taskName}.\n" +
                        "$javadocContains.\n" +
                        "Please, don't edit it manually unless for debugging.\n")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)

        if (superclass != null) {
            attrsBuilder = attrsBuilder.superclass(superclass)
        }

        attrsBuilder.addSuperinterface(ClassName.get("trikita.anvil", "Anvil", "AttributeSetter"))
        attrsBuilder.addStaticBlock(CodeBlock.of(
                "Anvil.registerAttributeSetter(new \$L());\n", outputClassName))

        var attrSwitch = MethodSpec.methodBuilder("set")
                .addParameter(ClassName.get("android.view", "View"), "v")
                .addParameter(ClassName.get("java.lang", "String"), "name")
                .addParameter(ParameterSpec.builder(TypeName.OBJECT, "arg")
                        .addModifiers(Modifier.FINAL).build())
                .addParameter(ParameterSpec.builder(TypeName.OBJECT, "old")
                        .addModifiers(Modifier.FINAL).build())
                .returns(TypeName.BOOLEAN)
                .addModifiers(Modifier.PUBLIC)

        var attrCases = CodeBlock.builder().beginControlFlow("switch (name)")

        var attrs = mutableListOf<Attr>()

        forEachView { view ->
            processViews(attrsBuilder, view)
            forEachMethod(view) { m, name, arg, isListener ->
                var attr: Attr?
                if (isListener) {
                    attr = listener(name, m, arg)
                } else {
                    attr = setter(name, m, arg)
                }
                if (attr != null) {
                    attrs.add(attr)
                }
            }
        }

        finalizeAttrs(attrs, attrsBuilder, attrCases)

        attrCases.endControlFlow()

        attrSwitch.addCode(attrCases.build()).addCode("return false;\n");

        attrsBuilder.addMethod(attrSwitch.build());

        JavaFile.builder(packageName, attrsBuilder.build())
                .build()
                .writeTo(project.file("src/$outputDirectory/java"))
    }

    fun forEachView(cb: (Class<*>) -> Unit) {
        val urls = mutableListOf(URL("jar", "", "file:${jarFile.absolutePath}!/"))
        for (dep in dependencies) {
            urls.add(URL("jar", "", "file:${dep.absolutePath}!/"))
        }
        val loader = URLClassLoader(urls.toTypedArray(), javaClass.classLoader)
        val viewClass = loader.loadClass("android.view.View")

        val jar = JarFile(jarFile)
        val list = Collections.list(jar.entries())
        list.sortBy { it.name }

        for (e in list) {
            if (e.name.endsWith(".class")) {
                val className = e.name.replace(".class", "").replace("/", ".")

                // Skip inner classes
                if (className.contains('$')) {
                    continue
                }
                try {
                    val c = loader.loadClass(className)
                    if (viewClass.isAssignableFrom(c) &&
                            java.lang.reflect.Modifier.isPublic(c.modifiers)) {
                        cb(c)
                    }
                } catch (ignored: NoClassDefFoundError) {
                    // Simply skip this class.
                    ignored.printStackTrace()
                }
            }
        }
    }

    fun forEachMethod(c: Class<*>, cb: (Method, String, Class<*>, Boolean) -> Unit) {
        val declaredMethods = c.declaredMethods.clone()
        declaredMethods.sortBy { it.name }
        for (m in declaredMethods) {
            if (!java.lang.reflect.Modifier.isPublic(m.modifiers) || m.isSynthetic || m.isBridge) {
                continue
            }

            val parameterType = getMethodParameterType(m) ?: continue

            if (m.name.matches(Regex("^setOn.*Listener$"))) {
                val name = m.name
                cb(m, "on" + name.substring(5, name.length - 8), parameterType, true)
            } else if (m.name.startsWith("set") && m.parameterCount == 1) {
                val name = Character.toLowerCase(m.name[3]).toString() + m.name.substring(4)
                cb(m, name, parameterType, false)
            }
        }
    }

    fun getMethodParameterType(m: Method): Class<*>? {
        if (m.parameterTypes.size == 0) {
            return null
        }

        val parameterType = m.parameterTypes[0]
        if (!java.lang.reflect.Modifier.isPublic(parameterType.modifiers)) {
            // If the parameter is not public then the method is inaccessible for us.
            return null
        } else if (m.annotations != null) {
            for (a in m.annotations) {
                // Don't process deprecated methods.
                if (a.annotationClass.equals(Deprecated::class)) {
                    return null
                }
            }
        } else if (m.declaringClass.canonicalName == "android.view.View") {
            return parameterType
        }

        // Check if the method overrode from a super class.
        var supClass = m.declaringClass.superclass
        while (true) {
            if (supClass == null) {
                break
            }
            try {
                supClass.getMethod(m.name, *m.parameterTypes)
                return null
            } catch (ignored: NoSuchMethodException) {
                // Intended to occur
            }

            if (supClass.canonicalName == "android.view.View") {
                break
            }
            supClass = supClass.superclass
        }
        return parameterType
    }

    //
    // Views generator functions:
    // For each view generates a function that calls v(C), where C is a view
    // class, e.g. FrameLayout.class => frameLayout() { v(FrameLayout.class) }
    //
    fun processViews(builder: TypeSpec.Builder, view: Class<*>) {
        val className = view.canonicalName
        var name = view.simpleName
        val extension = project.extensions.getByName("anvilgen") as AnvilGenPluginExtension

        val quirk = extension.quirks[className]
        if (quirk != null) {
            val alias = quirk["__viewAlias"]
            // if the whole view class is banned - do nothing
            if (alias == false) {
                return
            } else if (alias != null) {
                name = alias as String
            }
        }
        name = toCase(name, { c -> Character.toLowerCase(c) })
        val baseDsl = ClassName.get("trikita.anvil", "BaseDSL")
        val result = ClassName.get("trikita.anvil", "BaseDSL", "ViewClassResult")
        builder.addMethod(MethodSpec.methodBuilder(name)
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .returns(result)
                .addStatement("return \$T.v(\$T.class)", baseDsl, view)
                .build())
        builder.addMethod(MethodSpec.methodBuilder(name)
                .addParameter(ParameterSpec.builder(ClassName.get("trikita.anvil",
                        "Anvil", "Renderable"), "r").build())
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .returns(TypeName.VOID.box())
                .addStatement("return \$T.v(\$T.class, r)", baseDsl, view)
                .build())
    }

    //
    // Attrs generator functions
    //
    fun listener(name: String,
                 m: Method,
                 listenerClass: Class<*>): Attr? {
        val viewClass = m.declaringClass.canonicalName
        val listener = TypeSpec.anonymousClassBuilder("")
                .addSuperinterface(listenerClass)
        val declaredMethods = listenerClass.declaredMethods.clone()
        declaredMethods.sortBy { it.name }
        declaredMethods.forEach { lm ->
            val methodBuilder = MethodSpec.methodBuilder(lm.name)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(lm.returnType)

            var args = ""
            lm.parameterTypes.forEachIndexed { i, v ->
                methodBuilder.addParameter(v, "a$i")
                args += (if (i != 0) ", " else "") + "a$i"
            }

            if (lm.returnType.equals(Void.TYPE)) {
                methodBuilder
                        .addStatement("((\$T) arg).\$L($args)", listenerClass, lm.name)
                        .addStatement("\$T.render()", ClassName.get("trikita.anvil", "Anvil"))
            } else {
                methodBuilder
                        .addStatement("\$T r = ((\$T) arg).\$L($args)", lm.returnType, listenerClass, lm.name)
                        .addStatement("\$T.render()", ClassName.get("trikita.anvil", "Anvil"))
                        .addStatement("return r")
            }

            listener.addMethod(methodBuilder.build())
        }

        val attr = Attr(name, listenerClass, m)
        if (viewClass == "android.view.View") {
            attr.code.beginControlFlow("if (arg != null)", m.declaringClass)
                    .addStatement("v.${m.name}(\$L)", listener.build())
                    .nextControlFlow("else")
                    .addStatement("v.${m.name}((\$T) null)", listenerClass)
                    .endControlFlow()
                    .addStatement("return true")
            attr.unreachableBreak = true;
        } else {
            attr.code.beginControlFlow("if (v instanceof \$T && arg instanceof \$T)", m.declaringClass, listenerClass)
                    .beginControlFlow("if (arg != null)", m.declaringClass)
                    .addStatement("((\$T) v).${m.name}(\$L)", m.declaringClass,
                            listener.build())
                    .nextControlFlow("else")
                    .addStatement("((\$T) v).${m.name}((\$T) null)", m.declaringClass,
                            listenerClass)
                    .endControlFlow()
                    .addStatement("return true")
                    .endControlFlow()
        }
        return attr
    }

    fun setter(name: String,
               m: Method,
               argClass: Class<*>): Attr? {

        val viewClass = m.declaringClass.canonicalName
        val attr = Attr(name, argClass, m)

        val extension = project.extensions.getByName("anvilgen") as AnvilGenPluginExtension
        val quirks = extension.quirks[viewClass]
        if (quirks != null) {
            val closure = quirks["${m.name}:${argClass.canonicalName}"]
            if (closure != null) {
                return (closure as Closure<Attr?>).call(attr)
            } else {
                val nameClosure = quirks[m.name]
                if (nameClosure != null) {
                    return (nameClosure as Closure<Attr?>).call(attr)
                }
            }
        }
        val argBoxed = TypeName.get(argClass).box()
        if (viewClass == "android.view.View") {
            attr.code.beginControlFlow("if (arg instanceof \$T)", argBoxed)
                    .addStatement("v.${m.name}((\$T) arg)", argClass)
                    .addStatement("return true")
                    .endControlFlow()
        } else {
            attr.code.beginControlFlow("if (v instanceof \$T && arg instanceof \$T)", m.declaringClass, argBoxed)
                    .addStatement("((\$T) v).${m.name}((\$T) arg)", m.declaringClass, argClass)
                    .addStatement("return true")
                    .endControlFlow()
        }
        return attr;
    }

    fun addWrapperMethod(builder: TypeSpec.Builder, name: String, argClass: Class<*>) {
        val baseDsl = ClassName.get("trikita.anvil", "BaseDSL")
        val wrapperMethod = MethodSpec.methodBuilder(name)
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .addParameter(ParameterSpec.builder(argClass, "arg").build())
                .returns(TypeName.VOID.box())
                .addStatement("return \$T.attr(\$S, arg)", baseDsl, name)
        builder.addMethod(wrapperMethod.build())
    }

    fun finalizeAttrs(attrs: List<Attr>, dsl: TypeSpec.Builder, cases: CodeBlock.Builder) {
        attrs.sortedBy { it.name }.groupBy { it.name }.forEach {
            var all = it.value.sortedBy { it.param.name }
            var filered = all.filter { a ->
                !all.any { b ->
                    a != b && a.param == b.param &&
                            a.setter.declaringClass.isAssignableFrom(b.setter.declaringClass)
                }
            }

            cases.add("case \$S:\n", it.key)
            cases.indent();
            filered.filter { it.setter.declaringClass.canonicalName != "android.view.View" }.forEach {
                cases.add(it.code.build())
            }

            val common = filered.firstOrNull() { it.setter.declaringClass.canonicalName == "android.view.View" }
            if (common != null) {
                cases.add(common.code.build())
            }
            if (common == null || !common.unreachableBreak) {
                cases.add("break;\n")
            }
            cases.unindent();
        }

        attrs.sortedBy { it.name }.groupBy { it.name }.forEach {
            val name = it.key
            it.value.sortedBy { it.param.name }.groupBy { it.param }.forEach {
                addWrapperMethod(dsl, name, it.key)
            }
        }
    }

    fun toCase(s: String, fn: (Char) -> Char): String {
        return fn(s[0]).toString() + s.substring(1)
    }

    data class Attr(val name: String,
                    val param: Class<*>,
                    val setter: Method,
                    var unreachableBreak: Boolean = false,
                    val code: CodeBlock.Builder = CodeBlock.builder())
}
